﻿using Futaba.SBus.Telemetry.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using Futaba.SBus.Extensions;
using System.Diagnostics;

namespace Futaba.SBus.Telemetry
{
	/// <summary>
	/// Parses and returns the <see cref="SensorParameter">slot's</see> value based on the
	/// <see cref="SlotValueParserContext">context</see> supplied
	/// </summary>
	public class SlotValueParser
	{

		#region Singleton

		private static SlotValueParser _singletonSlotValueParser;
		private static readonly object SlotValueParserSingletonLock = new object();

		/// <summary>
		/// Singleton of the default implementation
		/// </summary>
		public static SlotValueParser DefaultInstance
		{
			get
			{
				if (_singletonSlotValueParser == null)
				{
					// ReSharper disable PossibleMultipleWriteAccessInDoubleCheckLocking
					lock (SlotValueParserSingletonLock)
					{
						if (_singletonSlotValueParser == null)
							_singletonSlotValueParser = new SlotValueParser();
					}
					// ReSharper restore PossibleMultipleWriteAccessInDoubleCheckLocking
				}
				return _singletonSlotValueParser;
			}
		}

		#endregion

		/// <summary>
		/// Processes the supplied <paramref name="ctx">context</paramref> and return's
		/// the slot's value
		/// </summary>
		/// <param name="ctx">The context to process</param>
		public double Parse(SlotValueParserContext ctx)
		{
			double newValue;

			if ((ctx.ParsingFlags & SensorValueParsingFlags.Coordinates) != 0)
				newValue = GetCoordinate(ctx);
			else if ((ctx.ParsingFlags & SensorValueParsingFlags.ByteValue) != 0)
				newValue = ctx.Data[1];
			else if ((ctx.ParsingFlags & SensorValueParsingFlags.IsUnsignedValue) != 0)
				newValue = GetUnsignedDoubleByteValue(ctx);
			else
				newValue = GetDoubleByteValue(ctx);

			if (ctx.ValueMultiplicator != 1)
				newValue *= ctx.ValueMultiplicator;

			if (IsBogusValue(ctx, newValue))
			{
#if DEBUG
				var dataText = BitConverter.ToString(ctx.Data);
				Debug.WriteLine("{0} has bogus value {1} (old: {3}) (Data: {2})", ctx.SlotId, newValue, dataText, ctx.CurrentValue);
#endif
				return ctx.CurrentValue.GetValueOrDefault();
			}

			return newValue;
		}

		/// <summary>
		/// Checks, if <see cref="SensorValueParsingFlags.BogusValueDetection"/> has been activated
		/// in the context and if the value seems out of range of plausible values
		/// </summary>
		/// <param name="ctx">The context to interpret</param>
		/// <param name="newValue">the value to check if it's bogus</param>
		/// <remarks>
		/// If the value seems bogus two times in a row, it is assumed to be ok
		/// </remarks>
		/// <returns></returns>
		protected bool IsBogusValue(SlotValueParserContext ctx, double newValue)
		{
			var oldBogus = ctx.LastBogusValue;
			ctx.LastBogusValue = 0;

			//don't allow 2 consecutive bogus values
			if (!ctx.CurrentValue.HasValue || oldBogus != 0)
				return false;

			bool bogusValueDetection = (ctx.ParsingFlags & SensorValueParsingFlags.BogusValueDetection) != 0;
			if (!bogusValueDetection)
				return false;

			//ignore new value, if it's less than 20% or more than 2x of the old value
			var diff = newValue / ctx.CurrentValue.Value;
			var isBogus = diff < 0.2 || diff > 2;

			if (isBogus)
				ctx.LastBogusValue = newValue;

			return isBogus;
		}

		/// <summary>
		/// Interprets the <see cref="SlotValueParserContext.Data"/> as BitConverter.ToInt16
		/// </summary>
		protected Int16 GetDoubleByteValue(SlotValueParserContext ctx)
		{
			byte[] parseValues = GetBytesForConversion(ctx);

			var newValue = BitConverter.ToInt16(parseValues, 0);
			return newValue;
		}

		/// <summary>
		/// Interprets the <see cref="SlotValueParserContext.Data"/> as BitConverter.ToUInt16
		/// </summary>
		protected UInt16 GetUnsignedDoubleByteValue(SlotValueParserContext ctx)
		{
			byte[] parseValues = GetBytesForConversion(ctx);

			var newValue = BitConverter.ToUInt16(parseValues, 0);
			return newValue;
		}

		private byte[] GetBytesForConversion(SlotValueParserContext ctx)
		{
			byte value1 = ctx.Data[0], value2 = ctx.Data[1];
			if ((ctx.ParsingFlags & SensorValueParsingFlags.Strip64) == SensorValueParsingFlags.Strip64)
			{
				if ((ctx.ParsingFlags & SensorValueParsingFlags.IsUnsignedValue) == SensorValueParsingFlags.IsUnsignedValue)
				{
					//Futaba sends 0x80 (ie. Voltage) as some kind of flag, maybe to signal that the sensor is active?!
					//example 80-3D actually should be 00-3D (61 == 6.1v)
					if (value1 == 0x80)
						value1 = 0;
				}
				//Futaba sends >= 0x40 (ie. Height) as some kind of flag, maybe to signal that the sensor is active?!
				//0x80 and above are negative values
				if (value1 >= 0x40 && value1 < 0x80)
					value1 -= 0x40;
			}
			byte[] parseValues = new[] { value2, value1 };

			if ((ctx.ParsingFlags & SensorValueParsingFlags.ReverseByteOrder) == SensorValueParsingFlags.ReverseByteOrder)
			{
				parseValues = parseValues.Reverse().ToArray();
			}
			return parseValues;
		}

		/// <summary>
		/// Interprets the <see cref="SlotValueParserContext.Data"/> as a GPS coordinate
		/// </summary>
		protected double GetCoordinate(SlotValueParserContext ctx)
		{
			if (ctx.Data.Length < 4)
				throw new ConfigurationException("Coordinates require 4 bytes (2 slots)!");

			var deg = ctx.Data[0];
			var min1 = ctx.Data[1];
			var min2 = ctx.Data[2];
			var min3 = ctx.Data[3];

			var min = BitConverter.ToInt32(new byte[] { min3, min2, min1, 0x00 }, 0) / 10000D;
			string coord = string.Format("{0}°{1}", deg, min.ToString(System.Globalization.CultureInfo.InvariantCulture));
			double newValue;
			if (!coord.TryParseCoordinate(out newValue))
				return 0;

			return newValue;
		}

	}
}